Passed
Pull Request — main (#3)
by Yuri
03:02 queued 01:33
created

index.ts ➔ compareBooleans   A

Complexity

Conditions 5

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 5
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 5
rs 9.3333
c 0
b 0
f 0
1
// Constants - make the code self-documenting
2 1
const NULLISH_VALUES = [null, undefined] as const;
3 1
const SORTABLE_PRIMITIVE_TYPES = ['string', 'number', 'boolean'] as const;
4
5
// Type definitions that make sense
6
type NullishValue = (typeof NULLISH_VALUES)[number];
7
type SortablePrimitiveType = (typeof SORTABLE_PRIMITIVE_TYPES)[number];
8
type ObjectType = Record<string | symbol, unknown>;
9
type SortedEntry = [string | symbol, unknown];
10
type NonSortableType =
11
  | Date
12
  | RegExp
13
  | (() => unknown)
14
  | ((...args: unknown[]) => unknown)
15
  | Error
16
  | Map<unknown, unknown>
17
  | Set<unknown>
18
  | WeakMap<object, unknown>
19
  | WeakSet<object>
20
  | Promise<unknown>;
21
22
type SortOptions = {
23
  ascending: boolean;
24
  sortPrimitiveArrays: boolean;
25
};
26
27 1
export function sort<T>(
28
  data: T,
29
  ascending = true,
30
  sortPrimitiveArrays = false
31
): T {
32 82
  const options: SortOptions = { ascending, sortPrimitiveArrays };
33 82
  return sortRecursively(data, options);
34
}
35
36
function sortRecursively<T>(data: T, options: SortOptions): T {
37 1726
  if (Array.isArray(data)) {
38 72
    return sortArray(data, options) as T;
39
  }
40
41 1654
  if (isPrimitive(data) || isNonSortableObject(data)) {
42 186
    return data;
43
  }
44
45 1467
  if (isObject(data)) {
46 1464
    return sortObject(data, options) as T;
47
  }
48
49 3
  return data;
50
}
51
52
function sortArray<T>(array: T[], options: SortOptions): T[] {
53 72
  if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
54 34
    return sortPrimitiveArray(array, options.ascending) as T[];
55
  }
56
57 106
  return array.map((item) => sortRecursively(item, options));
58
}
59
60
function sortObject(obj: ObjectType, options: SortOptions): ObjectType {
61 1464
  const entries = collectObjectEntries(obj);
62 1464
  const sortedEntries = sortObjectEntries(entries, options.ascending);
63 1464
  return createSortedObject(sortedEntries, options);
64
}
65
66
function shouldSortPrimitiveArray(
67
  array: unknown[],
68
  sortPrimitiveArrays: boolean
69
): boolean {
70 72
  return sortPrimitiveArrays && canSortPrimitiveArray(array);
71
}
72
73
function canSortPrimitiveArray(array: unknown[]): boolean {
74 45
  return (
75
    allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array)
76
  );
77
}
78
79
function allItemsAreSortablePrimitives(array: unknown[]): boolean {
80 45
  return array.every(isSortablePrimitive);
81
}
82
83
function sortPrimitiveArray(array: unknown[], ascending: boolean): unknown[] {
84
  // The array has already been validated as sortable in canSortPrimitiveArray
85
  // No need to check allItemsHaveSameSortableType again
86 168
  return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
87
}
88
89
function allItemsHaveSameSortableType(array: unknown[]): boolean {
90 34
  if (array.length === 0) return true;
91
92
  // Don't sort arrays that contain null or undefined values
93
  // as they represent absence of value and don't have a natural ordering
94 33
  if (hasNullishValues(array)) {
95
    return false;
96
  }
97
98
  // For arrays with only sortable primitives, check if they have the same type
99 33
  const firstItem = array[0];
100 33
  if (!isSortablePrimitive(firstItem)) return false;
101
102 33
  const expectedType = typeof firstItem;
103 120
  const allSameType = array.every((item) => typeof item === expectedType);
104
105
  // If all items are the same type, we can sort them normally
106 33
  if (allSameType) return true;
107
108
  // If we have mixed primitive types, we can still "sort" them
109
  // (the comparison function will return 0 for different types, maintaining order)
110
  // This allows us to cover the fallback case in compareSortablePrimitives
111 5
  return array.every(isSortablePrimitive);
112
}
113
114
function compareSortablePrimitives(
115
  a: unknown,
116
  b: unknown,
117
  ascending: boolean
118
): number {
119 168
  if (typeof a === 'string' && typeof b === 'string') {
120 40
    return ascending ? a.localeCompare(b) : b.localeCompare(a);
121
  }
122
123 128
  if (typeof a === 'number' && typeof b === 'number') {
124 83
    return compareNumbers(a, b, ascending);
125
  }
126
127 45
  if (typeof a === 'boolean' && typeof b === 'boolean') {
128 21
    return compareBooleans(a, b, ascending);
129
  }
130
131 24
  return 0; // Maintain order for other primitives
132
}
133
134
function compareNumbers(a: number, b: number, ascending: boolean): number {
135 83
  if (Number.isNaN(a) && Number.isNaN(b)) return 0;
136 81
  if (Number.isNaN(a)) return 1;
137 77
  if (Number.isNaN(b)) return -1;
138
139 71
  return ascending ? a - b : b - a;
140
}
141
142
function compareBooleans(a: boolean, b: boolean, ascending: boolean): number {
143 21
  if (a === b) return 0;
144 13
  if (a) return ascending ? 1 : -1;
145 6
  return ascending ? -1 : 1;
146
}
147
148
// Object entry handling
149
function collectObjectEntries(obj: ObjectType): SortedEntry[] {
150 1464
  const stringEntries = Object.entries(obj);
151 1464
  const symbolEntries = Object.getOwnPropertySymbols(obj).map(
152 5
    (symbol) => [symbol, obj[symbol]] as SortedEntry
153
  );
154
155 1464
  return [...stringEntries, ...symbolEntries];
156
}
157
158
function sortObjectEntries(
159
  entries: SortedEntry[],
160
  ascending: boolean
161
): SortedEntry[] {
162 1464
  return entries.sort(([keyA], [keyB]) =>
163 102
    compareObjectKeys(keyA, keyB, ascending)
164
  );
165
}
166
167
function compareObjectKeys(
168
  keyA: string | symbol,
169
  keyB: string | symbol,
170
  ascending: boolean
171
): number {
172 102
  if (typeof keyA === 'symbol' && typeof keyB === 'symbol') return 0;
173 100
  if (typeof keyA === 'symbol') return 1;
174 95
  if (typeof keyB === 'symbol') return -1;
175
176 95
  const stringA = keyA as string;
177 95
  const stringB = keyB as string;
178
179 95
  return ascending
180
    ? stringA.localeCompare(stringB)
181
    : stringB.localeCompare(stringA);
182
}
183
184
function createSortedObject(
185
  entries: SortedEntry[],
186
  options: SortOptions
187
): ObjectType {
188 1538
  const sortedEntries = entries.map(([key, value]) => [
189
    key,
190
    sortRecursively(value, options),
191
  ]);
192
193 59
  return Object.fromEntries(sortedEntries);
194
}
195
196
// Type guards - single responsibility, clear naming
197
function isNullish(value: unknown): value is NullishValue {
198 1649
  return NULLISH_VALUES.includes(value as NullishValue);
199
}
200
201
function hasNullishValues(array: unknown[]): boolean {
202 33
  return array.some(isNullish);
203
}
204
205
function isSortablePrimitive(
206
  value: unknown
207
): value is string | number | boolean {
208 1874
  return SORTABLE_PRIMITIVE_TYPES.includes(
209
    typeof value as SortablePrimitiveType
210
  );
211
}
212
213
function isPrimitive(data: unknown): boolean {
214 1654
  return isSortablePrimitive(data) || isNullish(data);
215
}
216
217
function isObject(data: unknown): data is ObjectType {
218 1467
  return typeof data === 'object' && data !== null;
219
}
220
221
function isNonSortableObject(obj: unknown): obj is NonSortableType {
222 1492
  const nonSortableTypes = [
223
    Date,
224
    RegExp,
225
    Function,
226
    Error,
227
    Map,
228
    Set,
229
    WeakMap,
230
    WeakSet,
231
    Promise,
232
  ];
233
234 1492
  return (
235 13287
    nonSortableTypes.some((type) => obj instanceof type) ||
236
    Symbol.iterator in Object(obj)
237
  );
238
}
239